﻿using System;
using System.Linq;
using System.Collections.Generic;
using System.Xml.Linq;
using CSW.Framework.Common;

namespace CSW.Framework.Linq.SchemaModel
{
    /// <summary>
    /// Database model.
    /// </summary>
    public class Database
    {
        private static readonly Dictionary<string, Database> m_TypeToSchema = new Dictionary<string, Database>();

        private readonly List<Table> m_Tables = new List<Table>();
        private string m_DatabaseName;

        /// <summary>
        /// Gets or sets the name of the database.
        /// </summary>
        /// <value>The name of the database.</value>
        public string DatabaseName
        {
            get { return m_DatabaseName; }
            set 
            { 
                m_DatabaseName = value;
                if (string.IsNullOrEmpty(ConnectionStringKey))
                    ConnectionStringKey = string.Format("{0}.ConnectionString", DatabaseName);
            }
        }

        /// <summary>
        /// Gets or sets the connection string configuration key.
        /// </summary>
        /// <value>The connection string configuration key.</value>
        public string ConnectionStringKey { get; set; }

        /// <summary>
        /// Gets or sets the provider.
        /// </summary>
        /// <value>The provider.</value>
        public string Provider { get; set; }

        /// <summary>
        /// Gets the tables.
        /// </summary>
        /// <value>The tables.</value>
        public List<Table> Tables
        {
            get { return m_Tables; }
        }

        /// <summary>
        /// Creates an XML representation of this instance.
        /// </summary>
        /// <returns>An XML representation of this instance.</returns>
        public XDocument CreateXml()
        {
            XDocument xml = new XDocument();

            XElement root = new XElement("Database");
            xml.Add(root);

            root.Add(new XElement("DatabaseName", DatabaseName));
            root.Add(new XElement("ConnectionStringKey", ConnectionStringKey));
            root.Add(new XElement("Provider", Provider));

            XElement tables = new XElement("Tables");
            root.Add(tables);

            foreach (Table table in m_Tables)
            {
                XElement tableElement = new XElement("Table");
                tables.Add(tableElement);

                tableElement.Add(new[] {
                    new XElement("Schema", table.Schema),
                    new XElement("TableName", table.TableName),
                    new XElement("FullClassName", table.FullClassName),
                    new XElement("ClassName", table.ClassName)});

                XElement properties = new XElement("Properties");
                tableElement.Add(properties);

                foreach (TableProperty property in table.Properties)
                {
                    properties.Add(new XElement("Property", new[] {
                        new XElement("PropertyName", property.PropertyName),
                        new XElement("DataType", property.DataType),
                        new XElement("ChildDataType", property.ChildDataType),
                        new XElement("IsChildContainer", property.IsChildContainer),
                        new XElement("IsChildList", property.IsChildList),
                        new XElement("IsPrimitiveType", property.IsPrimitiveType),
                        new XElement("ColumnName", property.Column != null ? property.Column.ColumnName : null),
                        new XElement("ChildColumnName", property.ChildColumnName)
                    }));
                }

                XElement columns = new XElement("Columns");
                tableElement.Add(columns);

                foreach (TableColumn column in table.Columns)
                {
                    columns.Add(new XElement("Column", new[] { 
                        new XElement("ColumnName", column.ColumnName),
                        new XElement("DbDataType", column.DbDataType),
                        new XElement("DefaultValue", column.DefaultValue),
                        new XElement("CharacterMaxLength", column.CharacterMaxLength),
                        new XElement("IsKey", column.IsKey),
                        new XElement("IsIdentity", column.IsIdentity),
                        new XElement("IsReadOnly", column.IsReadOnly),
                        new XElement("IsNullable", column.IsNullable),
                        new XElement("IsRowVersion", column.IsRowVersion),
                        new XElement("IsComputed", column.IsComputed),
                        new XElement("FKID", column.FKID),
                        new XElement("FKTableSchema", column.FKTableSchema),
                        new XElement("FKTableName", column.FKTableName),
                        new XElement("FKTableColumnName", column.FKTableColumnName)
                    }));
                }

                XElement uniqueConstraints = new XElement("UniqueConstraints");
                tableElement.Add(uniqueConstraints);

                foreach (UniqueConstraint uniqueConstraint in table.UniqueConstraints)
                {
                    XElement columnNames = new XElement("ColumnNames");
                    foreach (TableColumn column in uniqueConstraint.Columns)
                    {
                        columnNames.Add(new XElement("ColumnName", column.ColumnName));
                    }

                    uniqueConstraints.Add(
                        new XElement("UniqueConstraint", new[] {
                            new XElement("ConstraintName", uniqueConstraint.ConstraintName),
                            columnNames
                        }));
                }
            }

            return xml;
        }

        /// <summary>
        /// Gets the schema of a specified type.
        /// </summary>
        /// <typeparam name="T">The table type.</typeparam>
        /// <returns>The schema object.</returns>
        public static Database GetSchema<T>()
        {
            string fullClassName = typeof(T).FullName;
            return m_TypeToSchema[fullClassName];
        }

        /// <summary>
        /// Loads an XML representation of an instance of <see cref="Database"/>.
        /// </summary>
        /// <param name="xml">The XML.</param>
        /// <returns>A new instance of <see cref="Database"/>.</returns>
        public static Database LoadXml(XContainer xml)
        {
            Guard.ArgumentNotNull(xml, "xml");

            List<Table> tableList = new List<Table>();

            Func<string, int?> getIntNullable = p =>
                                                    {
                                                        int value;
                                                        if (int.TryParse(p, out value))
                                                            return value;
                                                        else
                                                            return null;
                                                    };

            XElement databaseElement = xml.Element("Database");
            string databaseName = (string)databaseElement.Element("DatabaseName");
            string connectionStringKey = (string)databaseElement.Element("ConnectionStringKey");
            string provider = (string)databaseElement.Element("Provider");

            XElement tablesElement = databaseElement.Element("Tables");
            foreach (XElement tableElement in tablesElement.Elements("Table"))
            {
                Table table = new Table
                {
                    ClassName = (string)tableElement.Element("ClassName"),
                    Schema = (string)tableElement.Element("Schema"),
                    TableName = (string)tableElement.Element("TableName"),
                    FullClassName = (string)tableElement.Element("FullClassName"),
                };

                foreach (XElement columnElement in tableElement.Element("Columns").Elements("Column"))
                {
                    table.Columns.Add(new TableColumn
                    {
                        CharacterMaxLength = getIntNullable((string)columnElement.Element("CharacterMaxLength")),
                        ColumnName = (string)columnElement.Element("ColumnName"),
                        DbDataType = (string)columnElement.Element("DbDataType"),
                        DefaultValue = (string)columnElement.Element("DefaultValue"),
                        FKID = getIntNullable((string)columnElement.Element("FKID")),
                        FKTableColumnName = (string)columnElement.Element("FKTableColumnName"),
                        FKTableName = (string)columnElement.Element("FKTableName"),
                        FKTableSchema = (string)columnElement.Element("FKTableSchema"),
                        IsIdentity = (bool)columnElement.Element("IsIdentity"),
                        IsKey = (bool)columnElement.Element("IsKey"),
                        IsNullable = (bool)columnElement.Element("IsNullable"),
                        IsReadOnly = (bool)columnElement.Element("IsReadOnly"),
                        IsRowVersion = (bool)columnElement.Element("IsRowVersion"),
                        IsComputed = (bool)columnElement.Element("IsComputed")
                    });
                }

                foreach (XElement propertyElement in tableElement.Element("Properties").Elements("Property"))
                {
                    TableProperty property = new TableProperty
                    {
                        PropertyName = (string)propertyElement.Element("PropertyName"),
                        DataType = (string)propertyElement.Element("DataType"),
                        ChildDataType = (string)propertyElement.Element("ChildDataType"),
                        IsChildContainer = (bool)propertyElement.Element("IsChildContainer"),
                        IsChildList = (bool)propertyElement.Element("IsChildList"),
                        IsPrimitiveType = (bool)propertyElement.Element("IsPrimitiveType"),
                        ChildColumnName = (string)propertyElement.Element("ChildColumnName")
                    };

                    string columnName = (string)propertyElement.Element("ColumnName");
                    if (!string.IsNullOrEmpty(columnName))
                    {
                        property.Column = table.Columns.Where(p => p.ColumnName == columnName).Single();
                    }

                    table.Properties.Add(property);
                }

                foreach (XElement uqConstraintElement in tableElement.Element("UniqueConstraints").Elements("UniqueConstraint"))
                {
                    UniqueConstraint uq = new UniqueConstraint { ConstraintName = (string)uqConstraintElement.Element("ConstraintName") };
                    foreach (XElement columnNameElement in uqConstraintElement.Element("ColumnNames").Elements("ColumnName"))
                    {
                        uq.Columns.Add(table.Columns.Where(p => p.ColumnName == (string)columnNameElement).Single());
                    }
                    table.UniqueConstraints.Add(uq);
                }

                tableList.Add(table);
            }

            Database db = new Database { DatabaseName = databaseName, ConnectionStringKey = connectionStringKey, Provider = provider };
            db.Tables.AddRange(tableList);

            foreach (Table table in tableList)
            {                
                if (m_TypeToSchema.ContainsKey(table.FullClassName))
                    m_TypeToSchema[table.FullClassName] = db;
                else
                    m_TypeToSchema.Add(table.FullClassName, db);
            }

            return db;
        }
    }
}
